/*
 * clock.c
 *
 * Copyright (C) 2018 Aleksandar Andrejevic <theflash@sdf.lonestar.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <clock.h>
#include <exception.h>
#include <syscalls.h>
#include <irq.h>
#include <user.h>
#include <timer.h>
#include <log.h>

#define RTC_IRQ 8

#define RTC_STA_UPDATING (1 << 7)

#define RTC_HOURS_PM_BIT (1 << 7)

#define RTC_STB_DST          (1 << 0)
#define RTC_STB_24HOUR       (1 << 1)
#define RTC_STB_BINARY       (1 << 2)
#define RTC_STB_SQUARE_WAVE  (1 << 3)
#define RTC_STB_UPDATE_INT   (1 << 4)
#define RTC_STB_ALARM_INT    (1 << 5)
#define RTC_STB_PERIODIC_INT (1 << 6)
#define RTC_STB_DISABLE      (1 << 7)

#define RTC_SECONDS_REG 0x00
#define RTC_MINUTES_REG 0x02
#define RTC_HOURS_REG 0x04
#define RTC_DAY_REG 0x07
#define RTC_MONTH_REG 0x08
#define RTC_YEAR_REG 0x09
#define RTC_STA_REG 0x0A
#define RTC_STB_REG 0x0B
#define RTC_STC_REG 0x0C

#define BCD_TO_BINARY(x) (((x) >> 4) * 10 + ((x) & 0x0F))

typedef struct
{
    byte_t second, minute, hour, day, month, year;
} rtc_time_t;

static clock_time_t clock_startup_timestamp = 0;
static clock_time_t clock_adjustment = 0;
static byte_t rtc_settings;

static void read_rtc(rtc_time_t *time)
{
    critical_t critical;
    enter_critical(&critical);

    cpu_write_port_byte(CMOS_CMD_PORT, RTC_STA_REG);
    for (;;) if (!(cpu_read_port_byte(CMOS_DATA_PORT) & RTC_STA_UPDATING)) break;

    cpu_write_port_byte(CMOS_CMD_PORT, RTC_SECONDS_REG);
    time->second = cpu_read_port_byte(CMOS_DATA_PORT);
    cpu_write_port_byte(CMOS_CMD_PORT, RTC_MINUTES_REG);
    time->minute = cpu_read_port_byte(CMOS_DATA_PORT);
    cpu_write_port_byte(CMOS_CMD_PORT, RTC_HOURS_REG);
    time->hour = cpu_read_port_byte(CMOS_DATA_PORT);
    cpu_write_port_byte(CMOS_CMD_PORT, RTC_DAY_REG);
    time->day = cpu_read_port_byte(CMOS_DATA_PORT);
    cpu_write_port_byte(CMOS_CMD_PORT, RTC_MONTH_REG);
    time->month = cpu_read_port_byte(CMOS_DATA_PORT);
    cpu_write_port_byte(CMOS_CMD_PORT, RTC_YEAR_REG);
    time->year = cpu_read_port_byte(CMOS_DATA_PORT);

    leave_critical(&critical);
}

sysret_t syscall_clock_get_time(clock_time_t *time)
{
    if (get_previous_mode() != KERNEL_MODE && !check_usermode(time, sizeof(clock_time_t))) return ERR_BADPTR;
    int64_t microseconds = (int64_t)(timer_get_nanoseconds() / 1000ULL);

    EH_TRY
    {
        *time = clock_startup_timestamp + clock_adjustment + microseconds;
    }
    EH_CATCH
    {
        EH_ESCAPE(return ERR_BADPTR);
    }
    EH_DONE;

    return ERR_SUCCESS;
}

sysret_t syscall_clock_set_time(clock_time_t *time)
{
    clock_time_t safe_time;

    if (get_previous_mode() == USER_MODE)
    {
        if (!check_usermode(time, sizeof(clock_time_t))) return ERR_BADPTR;

        EH_TRY safe_time = *time;
        EH_CATCH EH_ESCAPE(return ERR_BADPTR);
        EH_DONE;

        if (!check_privileges(PRIVILEGE_SET_TIME)) return ERR_FORBIDDEN;
    }
    else
    {
        safe_time = *time;
    }

    int64_t microseconds = (int64_t)(timer_get_nanoseconds() / 1000ULL);
    clock_time_t new_timestamp = safe_time;
    clock_time_t old_timestamp = clock_startup_timestamp + clock_adjustment + microseconds;
    clock_adjustment = new_timestamp - old_timestamp;

    // TODO: Update the RTC
    return ERR_SUCCESS;
}

void clock_init()
{
    cpu_write_port_byte(CMOS_CMD_PORT, RTC_STB_REG);
    rtc_settings = cpu_read_port_byte(CMOS_DATA_PORT);

    rtc_time_t time;
    clock_time_t adjustment;

    for (;;)
    {
        rtc_time_t check;
        read_rtc(&check);
        adjustment = (clock_time_t)(timer_get_nanoseconds() / 1000ULL);
        read_rtc(&time);
        if (memcmp(&time, &check, sizeof(rtc_time_t))) break;
    }

    if (!(rtc_settings & RTC_STB_BINARY))
    {
        time.second = BCD_TO_BINARY(time.second);
        time.minute = BCD_TO_BINARY(time.minute);
        time.hour = BCD_TO_BINARY(time.hour & ~RTC_HOURS_PM_BIT) | (time.hour & RTC_HOURS_PM_BIT);
        time.day = BCD_TO_BINARY(time.day);
        time.month = BCD_TO_BINARY(time.month);
        time.year = BCD_TO_BINARY(time.year);
    }

    if (!(rtc_settings & RTC_STB_24HOUR))
    {
        time.hour = (time.hour & RTC_HOURS_PM_BIT) ? ((time.hour & ~RTC_HOURS_PM_BIT) % 12) + 12 : time.hour % 12;
    }

    TRACE("Current time: 20%02u-%02u-%02u %02u:%02u:%02u\n",
          time.year,
          time.month,
          time.day,
          time.hour,
          time.minute,
          time.second);

    int i;
    clock_startup_timestamp = 0;

    for (i = 1980; i < (int)time.year + 2000; i++)
    {
        clock_startup_timestamp += (!(i % 400) || (!(i % 4) && (i % 100))) ? 31622400000000LL : 31536000000000LL;
    }

    int month_days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
    if (!(i % 400) || (!(i % 4) && (i % 100))) month_days[1]++;

    for (i = 1; i < time.month; i++)
    {
        clock_startup_timestamp += month_days[i - 1] * 86400000000LL;
    }

    clock_startup_timestamp += (time.day - 1) * 86400000000LL;
    clock_startup_timestamp += time.hour * 3600000000LL;
    clock_startup_timestamp += time.minute * 60000000LL;
    clock_startup_timestamp += time.second * 1000000LL;
    clock_startup_timestamp -= adjustment;
    TRACE("Startup timestamp: %lld\n", clock_startup_timestamp);
}
